/**
 * Copyright 2021 xpstem.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.xpstem.lelink.bluetooth;

import android.annotation.TargetApi;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.util.Log;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;

/**
 * @author billy zhang
 */
public class BluetoothManager {
    private static final String DEVICE_HAS_NOT_BLUETOOTH_MODULE = "device has not bluetooth module!";
    private static final String TAG = BluetoothManager.class.getSimpleName();
    private List<BtDeviceInfo> bondedList = new ArrayList<>();
    private HashMap<String, Object> paarMap = new HashMap<>();
    private List<BtDeviceInfo> newList = new ArrayList<>();
    private volatile Receiver receiver = new Receiver();
    private volatile Status status = Status.FREE;
    private BluetoothAdapter bleAdapter;
    private static volatile BluetoothManager singleInstance;
    private Context context;
    private boolean readVersion = true;
    private boolean supportBLE = false;

    private Optional<OnSearchListener> searchListener = Optional.empty();

    private enum Status {
        DISCOVERING,
        FREE
    }

    public interface OnSearchListener {
        /**
         * Call before discovery devices.
         */
        void onStartDiscovery();

        /**
         * Call when found a new device.
         *
         * @param device the new device
         */
        void onNewDeviceFound(BluetoothDevice device);

        /**
         * Call when the discovery process completed.
         *
         * @param bondedList the remote devices those are bonded(paired).
         * @param newList    the remote devices those are not bonded(paired).
         */
        void onSearchCompleted(List<BtDeviceInfo> bondedList, List<BtDeviceInfo> newList);

        void onError(Exception e);

    }

    private final BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {

        @Override
        public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
            if(!paarMap.containsKey(device.getAddress())){
                Log.i("ble", "device " + device.getAddress() + "   " + device.getName());
                paarMap.put(device.getAddress(), "mac:" + device.getAddress());
                BtDeviceInfo searchResult = new BtDeviceInfo(device, 0, rssi, null);
                newList.add(searchResult);
            }else{
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                    bleAdapter.stopLeScan(mLeScanCallback);
                    searchListener.ifPresent(l->l.onSearchCompleted(bondedList, newList));
                }
            }
        }

    };

    /**
     * Obtains the BtHelperClient getInstance the given context.
     *
     * @param context context
     * @return an instance of BtHelperClient
     */
    public static BluetoothManager getInstance(Context context) {
        if (singleInstance == null) {
            synchronized (BluetoothManager.class) {
                if (singleInstance == null)
                    singleInstance = new BluetoothManager(context);
            }
        }
        return singleInstance;
    }

    /**
     * private constructor for singleton
     *
     * @param context context
     */
    private BluetoothManager(Context context) {
        this.context = context.getApplicationContext();
        bleAdapter = BluetoothAdapter.getDefaultAdapter();
    }

    public void setOnSearchListener(OnSearchListener listener) {
        this.searchListener = Optional.ofNullable(listener);
    }

    public void setReadVersion(boolean readVersion) {
        this.readVersion = readVersion;
    }

    /**
     * Request for enable the device's bluetooth asynchronously.
     * Throw a NullPointerException if the device doesn't have a bluetooth module.
     */
    public void requestEnableBt() {
        if (bleAdapter == null) {
            throw new NullPointerException(DEVICE_HAS_NOT_BLUETOOTH_MODULE);
        }
        if (!bleAdapter.isEnabled())
            bleAdapter.enable();
    }

    /**
     * discovery the ble devices.
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    @SuppressWarnings("deprecation")
    public void searchBLEDevices() {
        try {
            checkNotNull(searchListener);
            if (bondedList == null) bondedList = new ArrayList<>();
            if (newList == null) newList = new ArrayList<>();
            if (bleAdapter == null) {
                searchListener.ifPresent(l->l.onError(new NullPointerException(DEVICE_HAS_NOT_BLUETOOTH_MODULE)));
                return;
            }
            if (bleAdapter.isDiscovering())
                bleAdapter.stopLeScan(mLeScanCallback);
            bleAdapter.startLeScan(mLeScanCallback);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * discovery the devices.
     */
    public void searchDevices() {
        try {
            if (status != Status.FREE) {
                return;
            }

            status = Status.DISCOVERING;
            checkNotNull(searchListener);
            if (bondedList == null) { bondedList = new ArrayList<>(); }
            if (newList == null) { newList = new ArrayList<>(); }
            if (bleAdapter == null) {
                searchListener.ifPresent(l->l.onError(new NullPointerException(DEVICE_HAS_NOT_BLUETOOTH_MODULE)));
                return;
            }
            receiver = new Receiver();
            // ACTION_FOUND
            IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
            context.registerReceiver(receiver, filter);
            // ACTION_DISCOVERY_FINISHED
            filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
            context.registerReceiver(receiver, filter);
            bondedList.clear();
            newList.clear();
            if (bleAdapter.isDiscovering()) {
                bleAdapter.cancelDiscovery();
            }
            bleAdapter.startDiscovery();
            searchListener.ifPresent(l->l.onStartDiscovery());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public android.bluetooth.BluetoothDevice getDevice(String macAddress) {
        return bleAdapter.getRemoteDevice(macAddress);
    }

    public void stopSearch() {
        bleAdapter.cancelDiscovery();
        status = Status.FREE;
    }

    public void refresh() {
        bondedList.clear();
        newList.clear();
        if (bleAdapter.isDiscovering()) {
            bleAdapter.cancelDiscovery();
        }
        bleAdapter.startDiscovery();
        status = Status.DISCOVERING;
    }

    /**
     * 搜索蓝牙广播
     */
    private class Receiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            try {
                String action = intent.getAction();
                if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)) {
                    searchListener.ifPresent(l->l.onStartDiscovery());
                } else if (android.bluetooth.BluetoothDevice.ACTION_FOUND.equals(action)) {
                    android.bluetooth.BluetoothDevice device = intent.getParcelableExtra(android.bluetooth.BluetoothDevice.EXTRA_DEVICE);
                    if (device.getBondState() == android.bluetooth.BluetoothDevice.BOND_NONE) {
                        if (paarMap != null && !paarMap.containsKey(device.getAddress())) {
                            paarMap.put(device.getAddress(), "mac:" + device.getAddress());
                            if (newList != null) {
                                int rssi = intent.getShortExtra(android.bluetooth.BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE);
                                BtDeviceInfo searchResult = new BtDeviceInfo(device, 0, rssi, null);
                                newList.add(searchResult);
                            }
                        }
                    } else if (device.getBondState() == android.bluetooth.BluetoothDevice.BOND_BONDED) {
                        if (bondedList != null) {
                            int rssi = intent.getShortExtra(android.bluetooth.BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE);
                            BtDeviceInfo searchResult = new BtDeviceInfo(device, 1, rssi, null);
                            bondedList.add(searchResult);
                        }
                    }
                } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
                    searchListener.ifPresent(l->l.onSearchCompleted(bondedList, newList));

                    searchBLEDevices();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Closes the connection and releases any system resources associated
     * with the stream.
     */
    public void close() {
        try {
            if (bleAdapter != null) {
                bleAdapter.cancelDiscovery();
                bleAdapter = null;
            }
            if (receiver != null) {
                context.unregisterReceiver(receiver);
                receiver = null;
            }

            newList = null;
            bondedList = null;
            singleInstance = null;
            status = Status.FREE;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 校验
     *
     * @param o
     */
    private void checkNotNull(Object o) {
        if (o == null)
            throw new NullPointerException();
    }


}
